
let string_of_status status = match status with
    Unix.WEXITED(i) -> "Exited with return code " ^ (string_of_int i)
  | Unix.WSIGNALED(i) -> "Killed with signal " ^ (string_of_int i)
  | Unix.WSTOPPED(i) -> "Stopped with signal " ^ (string_of_int i);;

let run_blocking command_line start_cb stdout_line_cb stderr_line_cb stop_cb =
  (* Split the command_line into an argument array. *)
  let argv_list = Str.split (Str.regexp " +") command_line in
  if List.length argv_list = 0 then
    Unix.WEXITED(-1)
  else
    let argv_array = Array.of_list argv_list in
    let prog = Array.get argv_array 0 in

    (* Open pipes for reading the subprocess' stdout and stderr. *)
    let stdout_r,stdout_w = Unix.pipe () in
    let stderr_r,stderr_w = Unix.pipe () in
    let stdin_r,stdin_w = Unix.pipe() in
    
    (* Launch the process, then close the ends of the pipes that we
       don't need. *)
    let pid = Unix.create_process prog argv_array stdin_r stdout_w stderr_w in
      Unix.close stdout_w;
      Unix.close stderr_w;
      Unix.close stdin_r;

      (* We've started, so call the start cb. *)
      start_cb pid;

      (* Thread function that reads lines from a channel until EOF, sending
	 each line to the given callback. *)
    let feed_lines_to_cb (ch, cb) =
      let rec feed_all_lines ch =
	cb ((input_line ch) ^ "\n");
	feed_all_lines ch
      in try feed_all_lines ch
	with End_of_file -> () in

    (* Set up reader threads for stdout and stderr. *)
    let stdout_reader_thread =
      Thread.create feed_lines_to_cb (Unix.in_channel_of_descr stdout_r,
				      stdout_line_cb) in
    let stderr_reader_thread =
      Thread.create feed_lines_to_cb (Unix.in_channel_of_descr stderr_r,
				      stderr_line_cb) in

    (* Wait for the process to finish. *)
    let _,status = Unix.waitpid [] pid in

      (* Join the reader threads. If we don't do this the process hangs. *)
      Thread.join stdout_reader_thread;
      Thread.join stderr_reader_thread;
      stop_cb status;
      status;;

type command = Run of string | Quit;;
type running_thread = Running of Thread.t | NotRunning;;

let command_to_string cmd = match cmd with
    Run command_line -> "Run(" ^ command_line ^ ")"
  | Quit -> "Quit";;

class runner =
object(self)
  val mutable start_cb = (fun _ -> ())
  val mutable stdout_cb = (fun _ -> ())
  val mutable stderr_cb = (fun _ -> ())
  val mutable stop_cb = (fun _ -> ())
  val mutable pid = -1
  val mutable thread = NotRunning
  val event_channel = Event.new_channel ()
  val mutex = Mutex.create ()
  method init () =
    let rec runner_thread () =
      let cmd = Event.sync (Event.receive event_channel) in
	match cmd with
	    Run command_line ->
	      let status = run_blocking command_line
		(fun p -> self#set_pid p; start_cb p)
		stdout_cb stderr_cb
		(fun s -> self#set_pid (-1); stop_cb s) in
		runner_thread()
	  | Quit -> () in
      thread <- Running(Thread.create runner_thread ());

  method reset_callbacks() =
    start_cb <- (fun _ -> ());
    stdout_cb <- (fun _ -> ());
    stderr_cb <- (fun _ -> ());
    stop_cb <- (fun _ -> ());

  method deinit () =
    Event.sync (Event.send event_channel Quit);
    (*Thread.join thread;*)
    thread <- NotRunning

  method private set_pid p =
    Mutex.lock mutex;
    pid <- p;
    Mutex.unlock mutex; ()

  method set_start_cb cb =
    Mutex.lock mutex; start_cb <- cb; Mutex.unlock mutex
  method set_stdout_cb cb =
    Mutex.lock mutex; stdout_cb <- cb; Mutex.unlock mutex;
  method set_stderr_cb cb =
   Mutex.lock mutex; stderr_cb <- cb; Mutex.unlock mutex;
  method set_stop_cb cb =
     Mutex.lock mutex; stop_cb <- cb; Mutex.unlock mutex;

  method is_running = (pid != -1)
      
  method run command_line =
    Mutex.lock mutex;
    Event.sync (Event.send event_channel (Run command_line));
    Mutex.unlock mutex;

  method kill () =
    if pid = -1 then 
      ()
    else
      Unix.kill pid 9
end
